import { lstat, mkdir, readdir, readFile, writeFile } from "fs/promises";
import { join } from "path";
import {
    GiveUpCompiling,
    GiveUpLoading,
    GiveUpParsing,
    GiveUpRelocating,
} from "./errors";
import {
    CompiledFile,
    FileReference,
    LoadedFile,
    RawFile,
    RelocatedFile,
    Plugin
} from "./definitions";
import { Output, Options } from "./options";

class Core {
    plugins: Plugin[] = [];
    includes?: string[];
    excludes?: string[];
    input: string;
    outputs: Output[];
    rootDir: string;

    constructor(rootDir: string, options: Options) {
        this.rootDir = rootDir;

        this.plugins.push(...(options.plugins ?? []));
        this.includes = options.includes;
        this.excludes = options.excludes;
        this.input = options.input;
        this.outputs = options.outputs;
    }

    async run(output: Output): Promise<void> {
        const plugins = [...this.plugins, ...(output.plugins ?? [])];
        await Promise.all(
            (
                await this.walkFiles()
            ).map((ref) =>
                this.load(plugins, ref)
                    .then(this.parse.bind(this, plugins))
                    .then(this.refactor.bind(this, plugins))
                    .then(this.compile.bind(this, plugins))
                    .then(this.relocate.bind(this, plugins))
                    .then(async (file) => {
                        await mkdir(join(output.dir, file.to.dirname), {
                            recursive: true,
                        });
                        await writeFile(
                            join(output.dir, file.to.fullPath),
                            file.compiled.compiled,
                        );
                    }),
            ),
        );
    }

    async walkFiles(): Promise<FileReference[]> {
        return this.#walkFiles(this.input, "/");
    }

    async load(plugins: Plugin[], file: FileReference): Promise<RawFile> {
        return (
            (
                await Promise.all(
                    plugins.map(async (plugin) => {
                        try {
                            if (!plugin.load) throw new GiveUpLoading();
                            const result = await plugin.load(file);
                            return {
                                success: true,
                                result,
                            };
                        } catch (e) {
                            if (e instanceof GiveUpLoading) {
                                return {
                                    success: false,
                                    result: undefined,
                                };
                            }
                            throw e;
                        }
                    }),
                )
            ).filter((result) => result.success)[0].result ||
            (async () => {
                return {
                    file,
                    raw: await readFile(file.fullPath),
                };
            })()
        );
    }

    async parse(plugins: Plugin[], file: RawFile): Promise<LoadedFile> {
        return (
            (
                await Promise.all(
                    plugins.map(async (plugin) => {
                        try {
                            if (!plugin.parse) throw new GiveUpParsing();
                            const result = await plugin.parse(file);
                            return {
                                success: true,
                                result,
                            };
                        } catch (e) {
                            if (e instanceof GiveUpParsing) {
                                return {
                                    success: false,
                                    result: undefined,
                                };
                            }
                            throw e;
                        }
                    }),
                )
            ).filter((result) => result.success)[0].result ||
            (() => {
                throw new Error(
                    `no plugin want to parse '${file.file.fullPath}!'`,
                );
            })()
        );
    }

    async refactor(plugins: Plugin[], file: LoadedFile): Promise<LoadedFile> {
        for (const plugin of plugins) {
            if (!plugin.refactor) continue;
            await plugin.refactor(file);
        }
        return file;
    }

    async compile(plugins: Plugin[], file: LoadedFile): Promise<CompiledFile> {
        return (
            (
                await Promise.all(
                    plugins.map(async (plugin) => {
                        try {
                            if (!plugin.compile) throw new GiveUpCompiling();
                            const result = await plugin.compile(file);
                            return {
                                success: true,
                                result,
                            };
                        } catch (e) {
                            if (e instanceof GiveUpCompiling) {
                                return {
                                    success: false,
                                    result: undefined,
                                };
                            }
                            throw e;
                        }
                    }),
                )
            ).filter((result) => result.success)[0].result ||
            (() => {
                throw new Error(
                    `no plugin want to compile '${file.file.fullPath}!'`,
                );
            })()
        );
    }

    async relocate(
        plugins: Plugin[],
        file: CompiledFile,
    ): Promise<RelocatedFile> {
        return (
            (
                await Promise.all(
                    plugins.map(async (plugin) => {
                        try {
                            if (!plugin.relocate) throw new GiveUpRelocating();
                            const result = await plugin.relocate(file);
                            return {
                                success: true,
                                result,
                            };
                        } catch (e) {
                            if (e instanceof GiveUpRelocating) {
                                return {
                                    success: false,
                                    result: undefined,
                                };
                            }
                            throw e;
                        }
                    }),
                )
            ).filter((result) => result.success)[0].result ||
            (() => {
                return {
                    to: file.file,
                    compiled: file,
                };
            })()
        );
    }

    async #walkFiles(base: string, dir: string): Promise<FileReference[]> {
        return (
            await Promise.all(
                (
                    await readdir(join(base, dir))
                ).map(async (file) => {
                    const fullPath = join(dir, file);
                    if ((await lstat(fullPath)).isDirectory()) {
                        return await this.#walkFiles(base, fullPath);
                    }
                    return [
                        {
                            dirname: dir,
                            filename: file,
                            fullPath,
                        } as FileReference,
                    ];
                }),
            )
        ).reduce((prev, current) => {
            return [...prev, ...current];
        });
    }
}

export {
    Core as MDPCore,
    GiveUpLoading,
    GiveUpParsing,
    GiveUpCompiling,
    GiveUpRelocating,
    Options as MDPOptions,
    Plugin as MDPPlugin,
    FileReference,
    RawFile,
    LoadedFile,
    CompiledFile,
    RelocatedFile,
};
